import {BufferAttribute, BufferGeometry, Color, FileLoader, Float32BufferAttribute, Loader, Vector3} from 'three';

/**
 * Description: A THREE loader for STL ASCII files, as created by Solidworks and other CAD programs.
 *
 * Supports both binary and ASCII encoded files, with automatic detection of type.
 *
 * The loader returns a non-indexed buffer geometry.
 *
 * Limitations:
 *  Binary decoding supports "Magics" color format (http://en.wikipedia.org/wiki/STL_(file_format)#Color_in_binary_STL).
 *  There is perhaps some question as to how valid it is to always assume little-endian-ness.
 *  ASCII decoding assumes file is UTF-8.
 *
 * Usage:
 *  const loader = new STLLoader();
 *  loader.load( './models/stl/slotted_disk.stl', function ( geometry ) {
 *    scene.add( new THREE.Mesh( geometry ) );
 *  });
 *
 * For binary STLs geometry might contain colors for vertices. To use it:
 *  // use the same code to load STL as above
 *  if (geometry.hasColors) {
 *    material = new THREE.MeshPhongMaterial({ opacity: geometry.alpha, vertexColors: true });
 *  } else { .... }
 *  const mesh = new THREE.Mesh( geometry, material );
 *
 * For ASCII STLs containing multiple solids, each solid is assigned to a different group.
 * Groups can be used to assign a different color by defining an array of materials with the same length of
 * geometry.groups and passing it to the Mesh constructor:
 *
 * const mesh = new THREE.Mesh( geometry, material );
 *
 * For example:
 *
 *  const materials = [];
 *  const nGeometryGroups = geometry.groups.length;
 *
 *  const colorMap = ...; // Some logic to index colors.
 *
 *  for (let i = 0; i < nGeometryGroups; i++) {
 *
 *        const material = new THREE.MeshPhongMaterial({
 *            color: colorMap[i],
 *            wireframe: false
 *        });
 *
 *  }
 *
 *  materials.push(material);
 *  const mesh = new THREE.Mesh(geometry, materials);
 */


class STLLoader extends Loader {

    constructor(manager) {

        super(manager);

    }

    load(url, onLoad, onProgress, onError) {

        const scope = this;

        const loader = new FileLoader(this.manager);
        loader.setPath(this.path);
        loader.setResponseType('arraybuffer');
        loader.setRequestHeader(this.requestHeader);
        loader.setWithCredentials(this.withCredentials);

        loader.load(url, function (text) {

            try {

                onLoad(scope.parse(text));

            } catch (e) {

                if (onError) {

                    onError(e);

                } else {

                    console.error(e);

                }

                scope.manager.itemError(url);

            }

        }, onProgress, onError);

    }

    parse(data) {

        function isBinary(data) {

            const reader = new DataView(data);
            const face_size = (32 / 8 * 3) + ((32 / 8 * 3) * 3) + (16 / 8);
            const n_faces = reader.getUint32(80, true);
            const expect = 80 + (32 / 8) + (n_faces * face_size);

            if (expect === reader.byteLength) {

                return true;

            }

            // An ASCII STL data must begin with 'solid ' as the first six bytes.
            // However, ASCII STLs lacking the SPACE after the 'd' are known to be
            // plentiful.  So, check the first 5 bytes for 'solid'.

            // Several encodings, such as UTF-8, precede the text with up to 5 bytes:
            // https://en.wikipedia.org/wiki/Byte_order_mark#Byte_order_marks_by_encoding
            // Search for "solid" to start anywhere after those prefixes.

            // US-ASCII ordinal values for 's', 'o', 'l', 'i', 'd'

            const solid = [115, 111, 108, 105, 100];

            for (let off = 0; off < 5; off++) {

                // If "solid" text is matched to the current offset, declare it to be an ASCII STL.

                if (matchDataViewAt(solid, reader, off)) return false;

            }

            // Couldn't find "solid" text at the beginning; it is binary STL.

            return true;

        }

        function matchDataViewAt(query, reader, offset) {

            // Check if each byte in query matches the corresponding byte from the current offset

            for (let i = 0, il = query.length; i < il; i++) {

                if (query[i] !== reader.getUint8(offset + i)) return false;

            }

            return true;

        }

        function parseBinary(data) {

            const reader = new DataView(data);
            const faces = reader.getUint32(80, true);

            let r, g, b, hasColors = false, colors;
            let defaultR, defaultG, defaultB, alpha;

            // process STL header
            // check for default color in header ("COLOR=rgba" sequence).

            for (let index = 0; index < 80 - 10; index++) {

                if ((reader.getUint32(index, false) == 0x434F4C4F /*COLO*/) &&
                    (reader.getUint8(index + 4) == 0x52 /*'R'*/) &&
                    (reader.getUint8(index + 5) == 0x3D /*'='*/)) {

                    hasColors = true;
                    colors = new Float32Array(faces * 3 * 3);

                    defaultR = reader.getUint8(index + 6) / 255;
                    defaultG = reader.getUint8(index + 7) / 255;
                    defaultB = reader.getUint8(index + 8) / 255;
                    alpha = reader.getUint8(index + 9) / 255;

                }

            }

            const dataOffset = 84;
            const faceLength = 12 * 4 + 2;

            const geometry = new BufferGeometry();

            const vertices = new Float32Array(faces * 3 * 3);
            const normals = new Float32Array(faces * 3 * 3);

            const color = new Color();

            for (let face = 0; face < faces; face++) {

                const start = dataOffset + face * faceLength;
                const normalX = reader.getFloat32(start, true);
                const normalY = reader.getFloat32(start + 4, true);
                const normalZ = reader.getFloat32(start + 8, true);

                if (hasColors) {

                    const packedColor = reader.getUint16(start + 48, true);

                    if ((packedColor & 0x8000) === 0) {

                        // facet has its own unique color

                        r = (packedColor & 0x1F) / 31;
                        g = ((packedColor >> 5) & 0x1F) / 31;
                        b = ((packedColor >> 10) & 0x1F) / 31;

                    } else {

                        r = defaultR;
                        g = defaultG;
                        b = defaultB;

                    }

                }

                for (let i = 1; i <= 3; i++) {

                    const vertexstart = start + i * 12;
                    const componentIdx = (face * 3 * 3) + ((i - 1) * 3);

                    vertices[componentIdx] = reader.getFloat32(vertexstart, true);
                    vertices[componentIdx + 1] = reader.getFloat32(vertexstart + 4, true);
                    vertices[componentIdx + 2] = reader.getFloat32(vertexstart + 8, true);

                    normals[componentIdx] = normalX;
                    normals[componentIdx + 1] = normalY;
                    normals[componentIdx + 2] = normalZ;

                    if (hasColors) {

                        color.set(r, g, b).convertSRGBToLinear();

                        colors[componentIdx] = color.r;
                        colors[componentIdx + 1] = color.g;
                        colors[componentIdx + 2] = color.b;

                    }

                }

            }

            geometry.setAttribute('position', new BufferAttribute(vertices, 3));
            geometry.setAttribute('normal', new BufferAttribute(normals, 3));

            if (hasColors) {

                geometry.setAttribute('color', new BufferAttribute(colors, 3));
                geometry.hasColors = true;
                geometry.alpha = alpha;

            }

            return geometry;

        }

        function parseASCII(data) {

            const geometry = new BufferGeometry();
            const patternSolid = /solid([\s\S]*?)endsolid/g;
            const patternFace = /facet([\s\S]*?)endfacet/g;
            const patternName = /solid\s(.+)/;
            let faceCounter = 0;

            const patternFloat = /[\s]+([+-]?(?:\d*)(?:\.\d*)?(?:[eE][+-]?\d+)?)/.source;
            const patternVertex = new RegExp('vertex' + patternFloat + patternFloat + patternFloat, 'g');
            const patternNormal = new RegExp('normal' + patternFloat + patternFloat + patternFloat, 'g');

            const vertices = [];
            const normals = [];
            const groupNames = [];

            const normal = new Vector3();

            let result;

            let groupCount = 0;
            let startVertex = 0;
            let endVertex = 0;

            while ((result = patternSolid.exec(data)) !== null) {

                startVertex = endVertex;

                const solid = result[0];

                const name = (result = patternName.exec(solid)) !== null ? result[1] : '';
                groupNames.push(name);

                while ((result = patternFace.exec(solid)) !== null) {

                    let vertexCountPerFace = 0;
                    let normalCountPerFace = 0;

                    const text = result[0];

                    while ((result = patternNormal.exec(text)) !== null) {

                        normal.x = parseFloat(result[1]);
                        normal.y = parseFloat(result[2]);
                        normal.z = parseFloat(result[3]);
                        normalCountPerFace++;

                    }

                    while ((result = patternVertex.exec(text)) !== null) {

                        vertices.push(parseFloat(result[1]), parseFloat(result[2]), parseFloat(result[3]));
                        normals.push(normal.x, normal.y, normal.z);
                        vertexCountPerFace++;
                        endVertex++;

                    }

                    // every face have to own ONE valid normal

                    if (normalCountPerFace !== 1) {

                        console.error('THREE.STLLoader: Something isn\'t right with the normal of face number ' + faceCounter);

                    }

                    // each face have to own THREE valid vertices

                    if (vertexCountPerFace !== 3) {

                        console.error('THREE.STLLoader: Something isn\'t right with the vertices of face number ' + faceCounter);

                    }

                    faceCounter++;

                }

                const start = startVertex;
                const count = endVertex - startVertex;

                geometry.userData.groupNames = groupNames;

                geometry.addGroup(start, count, groupCount);
                groupCount++;

            }

            geometry.setAttribute('position', new Float32BufferAttribute(vertices, 3));
            geometry.setAttribute('normal', new Float32BufferAttribute(normals, 3));

            return geometry;

        }

        function ensureString(buffer) {

            if (typeof buffer !== 'string') {

                return new TextDecoder().decode(buffer);

            }

            return buffer;

        }

        function ensureBinary(buffer) {

            if (typeof buffer === 'string') {

                const array_buffer = new Uint8Array(buffer.length);
                for (let i = 0; i < buffer.length; i++) {

                    array_buffer[i] = buffer.charCodeAt(i) & 0xff; // implicitly assumes little-endian

                }

                return array_buffer.buffer || array_buffer;

            } else {

                return buffer;

            }

        }

        // start

        const binData = ensureBinary(data);

        return isBinary(binData) ? parseBinary(binData) : parseASCII(ensureString(data));

    }

}

export {STLLoader};
